
# Copyright (c) 2014, Franklin Brauning V.  All rights reserved.
# 
#    El desarrollo inicial de esta utilidad fue producido
#    por Franklin Brauning <brauningbar@gmail.com>
# 
#    Si consideras que este codigo puede serte util, eres libre
#    de usarlo a conveniencia, pero no olvides mantener la licencia
#    y dar credito al autor.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#   * Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
# 
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
# 
#   * Neither the name of the author nor the names of its contributors may
#     be used to endorse or promote products derived from this software without
#     specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from django.db import models
from django.db.transaction import atomic
from .base import *
from .tagged import *
from .fossil import *
from .views import *

def show_links(func):
	''' composable __str__ decorator to debug tidyfs models '''
	def w(self):
		s,ats = func(self)
		if self.prevlink:
			ats.append("prevlink:"+str(self.prevlink))
		if self.nextlink:
			ats.append("nextlink:"+str(self.nextlink))
		return s,ats
	return w


# Base tag namespace for tidyfs denoting tags acting over
# the actual node
STICKYTAG = 1
# Base tag namespace for tifyfs denoting tags acting over
# the actual node and its descendants
RECTAG = 2


class tidyfs(linked_content, tagged):
	''' this class defines a hierarchic structure to model an easy
	to deploy and low performance filesystem over sql.

	HOWTO SUBCLASS
	-----------

	Create a grouping with this versioned relation

	exec(build_grouping(
	  itemmodels=[(YOUR-GROUPMODEL, 'child', 'children'), ...]
	  groupmodel=YOUR-GROUPMODEL
	  groupabstract=__YOUR-GROUPMODEL
	  ...

	class YOUR-GROUPMODEL(..., tidyfs, __YOUR-GROUPMODEL, ...)


	TODO
	----
	Find and resolve conflicts/corrupcion with a fsck-like process


	Tagging
	-------

	Nodes can be tagged in two ways:
	STICKYTAG
	The tag applies only to the actual node. Example:

	>>> node.xsettag(STICKYTAG, 'x', 100)

	RECTAG
	The tag applies to the actual node and its descendats. Example:

	>>> node.xsettag(RECTAG, 'x', 200)

	** STICKYTAG has higher precedence than RECTAG. Example:

	>>> node.probe_tag('x')       ==> 100


	Consistent state for operations involving several nodes at once
	---------------------------------------------------------------

	Cases:
	  - Reasigning a child node. After undo/redo restore the structure
	  to the previous consistent state of involved nodes


	** Subclasses have to define the data contained in the tree nodes.
	'''
	class Meta:
		abstract = True

	parent = models.ForeignKey('self', null=True, related_name='_parent_')

	def isroot(self):
		''' True if this node has no parent '''
		return not bool(self.parent)

	def empty(self):
		''' True if this node doesnt have children '''
		return not bool(list(tips(self.get_active_children())))

	def childs(self):
		''' get all current children (queryset) '''
		ch = self.get_fossil_children()
		if not self.isroot():
			return filter(lambda c:c.id!=self.parent.id, ch)
		return ch

	def brothers(self):
		''' get brother nodes (queryset) '''
		if self.isroot():
			return fossils(self.__class__.objects.filter(
			  parent__isnull=True)).exclude(id=self.id)
		else:
			return self.parent.childs().exclude(id=self.id)

	def findpath(self, *path):
		r = self
		for p in path:
			ts = tips(r.get_active_children())
			try:
				r = next(filter(lambda n:n.nombre==p, ts))
			except StopIteration:
				return
		return r

	@atomic
	def recreatepath(self, *path, initnode=None, touchnode=None, find=True):
		r = self
		for p in path:
			if find:
				ts = tips(r.get_active_children())
				try:
					r = next(filter(lambda n:n.nombre==p, ts))
					continue
				except StopIteration:
					pass
			s = initnode() if initnode else self.__class__()
			s.nombre = p
			if touchnode:
				touchnode(s)
			r.link_child(s)
			r = s
		return r

	# UNTESTED
	def unlink_parent(self, **kw):
		self.parent.unlink_child(self, **kw)

	# UNTESTED
	@atomic
	def unlink_child(self, obj, check=False):
		self._mark_propagate_links(True)
		obj._mark_propagate_links(True)
		if check:
			raise Exception('unlink_child:check=True no esta implementado')
		self.remove_child(obj)
		obj.parent = None
		self._link(self, obj, lextreme=True, rextreme=True, clear=True)
		self.save()
		obj.save()
		self._mark_propagate_links(False)
		obj._mark_propagate_links(False)

	@atomic
	def link_child(self, obj):
		if self.islinked() or obj.islinked():
			raise InvalidFossil("link_child: no procede en nodos ya enlazados")

		if obj.parent:
			if obj.parent.islinked():
				raise InvalidFossil("link_child: actual parent ya enlazado")
			#obj.parent._mark_propagate_links(True)
			obj.parent.remove_child(obj)
			obj.parent.save()
		self._mark_propagate_links(True)
		obj._mark_propagate_links(True)
		obj.parent = self
		obj.ensuremodified()
		obj.save()
		self.add_child(obj)
		self.save()
		self._mark_propagate_links(False)
		obj._mark_propagate_links(False)

	# UNTESTED viernes 5 septiembre
	@atomic
	def __link_child(self, obj):
		''' link a child to the current node.

		This implementation keeps track of the previous state
		of involved nodes (child, self) so undo/redo will always
		keep fs overall consistency
		'''
		# obj.parent - obj - self
		#

		if self.islinked() or obj.islinked():
			raise InvalidFossil("link_child: no procede en nodos ya enlazados")

		if obj.parent:
			if obj.parent.islinked():
				raise InvalidFossil("link_child: actual parent ya enlazado")
			self._link(obj.parent, obj, lextreme=True)
			#obj.parent._mark_propagate_links(True)
			obj.parent.remove_child(obj)
			obj.parent.save()
		self._mark_propagate_links(True)
		obj._mark_propagate_links(True)
		self._link(obj, self, lextreme=(not bool(obj.prevlink)), rextreme=True)
		obj.parent = self
		obj.ensuremodified()
		obj.save()
		self.add_child(obj)
		self.save()
		self._mark_propagate_links(False)
		obj._mark_propagate_links(False)

	def findroot(self):
		x = self
		while x.parent:
			x = x.parent
		return x

	def iter(self, f):
		f(self)
		for c in self.childs():
			c.iter()

	def iter_depth(self, f):
		for c in self.childs():
			c.iter_depth()
		f(self)

	def lineage(self, use_self=False):
		''' get ancestors in reverse (iterator) '''
		x = self if use_self else self.parent
		while x:
			yield x
			x = x.parent

	def lineage_use_self(self):
		''' get ancestors in reverse starting with self (iterator). '''
		for x in self.lineage(use_self=True):
			yield x

	# TODO: use yield messages
	def probe_lineage(self, p, **kw):
		''' traverse ancestors and control with the functional provided '''
		for x in self.lineage(**kw):
			y = p(x)
			if y:
				return y

	def probe_lineage_use_self(self, p):
		return self.probe_lineage(p, use_self=True)

	def probe_tag(self, t):
		''' get tag for current node.

		The implementation considers STICKYTAG/RECTAG tag namespaces
		'''
		return self.xgettag(STICKYTAG, t) or \
		       self.probe_lineage_use_self(
		         lambda node:node.xgettag(RECTAG, t))

	def join_tags(self):
		''' get a view of all tags for the current node.
		Returns dict with STICKY and RECTAG namespaces merged
		'''
		ts = {}
		for d in reversed([n.accesstags() for n in self.lineage_use_self()]):
			ts.update(d.get(RECTAG, {}))
		ts.update(self.gettag(STICKYTAG, {}))
		return ts

	def gen_html(self, **kw):
		''' helper function to generate output related to this tree.

		Initially coded to solve the emision of html required
		to present jquery accordion widgets, but generic enough
		to emit almost any output that requires traversing and
		trimming unwanted nodes from the output.

		Parameters (named parameters):
		  - contents_map:
			  dictionary containing keys on every node wanted to emit
			  output. Nodes not declared here will be trimmed if there
			  arent descendants emiting output.

			  For every key there is a function that receives the actual
			  node and a functional to apply a function over the
			  children's html output (in wrappered boolean form)

			  If you ever need to emit empty strings use the some(...) helper
			  function to let the algorithm understand this, otherwise empty
			  strings will be treated as no output.

			  When the output for every node doesnt have specialized
			  heuristics, you can just provide a defaultdict mapping to
			  all nodes.

		  - envelop_gen:
			  functional. Receives current node's output (including
			  children's) and refines it.  When generating html, this
			  should wrap currents node output in '<div>' tags, for example.

			  Consider that when a node doesnt generate output even though
			  some descendant does, this function is expected to emit
			  valid output resembling this node without contents

		  - dep_gen (optional):
		      output generator for nodes not declared in contents_gen but
		      requiring to be output as some of its descendants will
		      be too. In its form, is similar to 'contents_map' functions.
		      The default is expected to work in most cases.

		For example, to emit jquery accordions:

		self.gen_html(
		  envelop_gen=lambda node,contents:
		                "<div><h>%s</h>%s</div>" % (node.name, contents),
		  contents_map=defaultdict(
		                 lambda:
		                   lambda node:
		                     ("<div>",
		                      "This node is named %s</div>" % node.name))
		               },
		'''
		from collections import defaultdict
		contents_gen = kw['contents_gen']
		if not isinstance(contents_gen, defaultdict):
			old = contents_gen
			contents_gen = defaultdict(lambda:lambda *a, **kw:None)
			contents_gen.update(old)
			kw['contents_gen'] = contents_gen

			def default_dep_gen(node, subs):
				return "".join(map(somedata, subs(lambda x:x)))
			dep_gen = kw.get('dep_gen', default_dep_gen)

			for node, proc in list(contents_gen.items()):
				for n in node.lineage():
					if n not in contents_gen:
						contents_gen[n] = dep_gen

		return somedata(self._gen_html(**kw), "")

	# this code (and its relateds) is optimized to traverse tree lazily (ala Haskell).
	# If you ever modify this give it a good read before editing!!
	# FIXME: ...even though string concatenations suck
	#        To fix this, keep the traverse but make concatenations all at once
	#        before returning final output
	def _gen_html(self, **kw):
		subs_good = None
		def subs_good_apply(f):
			nonlocal subs_good
			if subs_good==None:
				childs = tips(self.childs())
				subs_good = tuple(filter(bool, map(lambda x:x._gen_html(**kw), childs)))
			return f(subs_good)

		content_gen = kw['contents_gen'][self]
		thisdata = ensure_optional(content_gen(self, subs_good_apply))
		if thisdata:
			envelop = kw['envelop_gen']
			return some(envelop(self, somedata(thisdata)))

class some:
	def __init__(self, data):
		self.data = data

	def __bool__(self):
		return True

def somedata(x, default=None):
	return x.data if x else default

def ensure_optional(x):
	if type(x) is some:
		return x
	return some(x) if x else None

def with_data(func, *args):
	args = map(ensure_optional, args)
	if all(args):
		return func(*map(somedata, args))

def with_any_data(func, *args):
	args = map(ensure_optional, args)
	if any(args):
		return func(*args)

def with_lazydata(func, *args):
	args = list(args)
	for i,x in enumerate(args):
		x = ensure_optional(x())
		if not x:
			return
		args[i] = somedata(x)
	return func(*args)

def with_any_lazydata(func, *args):
	args = [ensure_optional(x()) for x in args]
	if any(args):
		return func(*args)
